using LightCAD.MathLib;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace LightCAD.Three
{

    public class Line : Object3D, IGeometry, IMorphTargets, IMaterialObject, ISolid
    {
        #region scope properties or methods
        //private static Vector3 _start = new Vector3();
        //private static Vector3 _end = new Vector3();
        //private static Matrix4 _inverseMatrix = new Matrix4();
        //private static Ray _ray = new Ray();
        //private static Sphere _sphere = new Sphere();
        private static LineContext getContext()
            => ThreeThreadContext.GetCurrThreadContext().LineCtx;
        #endregion

        #region Properties

        public BufferGeometry geometry;
        public ListEx<Material> materials;
        #endregion

        #region properties
        public Material material
        {
            get => this.materials[0];
            set
            {
                this.materials[0] = value;
            }
        }

        public ListEx<double> morphTargetInfluences { get; set; }
        public JsObj<int> morphTargetDictionary { get; set; }
        #endregion

        #region constructor
        public Line(BufferGeometry geometry = null, params Material[] material) : base()
        {
            if (geometry == null) geometry = new BufferGeometry();
            if (material == null) material = new Material[] { new LineBasicMaterial() };
            this.type = "Line";
            this.geometry = geometry;
            this.materials = material.ToListEx();
            this.updateMorphTargets();
        }
        #endregion

        #region methods
        public Line copy(Line source, bool recursive)
        {
            base.copy(source, recursive);
            this.materials = source.materials;
            this.geometry = source.geometry;
            return this;
        }
        public virtual Line computeLineDistances()
        {
            var geometry = this.geometry;
            // we assume non-indexed geometry
            if (geometry.index == null)
            {
                var ctx = getContext();
                var _start = ctx._start;
                var _end = ctx._end;
                var positionAttribute = geometry.attributes.position;
                var lineDistances = new ListEx<double> { 0 };
                for (int i = 1, l = positionAttribute.count; i < l; i++)
                {
                    _start.FromBufferAttribute(positionAttribute, i - 1);
                    _end.FromBufferAttribute(positionAttribute, i);
                    lineDistances[i] = lineDistances[i - 1];
                    lineDistances[i] += _start.DistanceTo(_end);
                }
                geometry.setAttribute("lineDistance", new Float32BufferAttribute(lineDistances.ToArray(), 1));
            }
            else
            {
                console.warn("THREE.Line.computeLineDistances(): Computation only possible with non-indexed BufferGeometry.");
            }
            return this;
        }
        public override void raycast(Raycaster raycaster, ListEx<Raycaster.Intersection> intersects, object vars = null)
        {
            var geometry = this.geometry;
            var matrixWorld = this.matrixWorld;
            var threshold = raycaster._params.Line["threshold"];
            var drawRange = geometry.drawRange;
            // Checking boundingSphere distance to ray
            if (geometry.boundingSphere == null)
                geometry.computeBoundingSphere();
            var ctx = getContext();
            var _sphere = ctx._sphere;
            var _inverseMatrix = ctx._inverseMatrix;
            var _ray = ctx._ray;
            _sphere.Copy(geometry.boundingSphere);
            _sphere.ApplyMatrix4(matrixWorld);
            _sphere.Radius += threshold;
            if (raycaster.ray.IntersectsSphere(_sphere) == false) return;
            //
            _inverseMatrix.Copy(matrixWorld).Invert();
            _ray.Copy(raycaster.ray).ApplyMatrix4(_inverseMatrix);
            var localThreshold = threshold / ((this.scale.X + this.scale.Y + this.scale.Z) / 3);
            var localThresholdSq = localThreshold * localThreshold;
            var vStart = new Vector3();
            var vEnd = new Vector3();
            var interSegment = new Vector3();
            var interRay = new Vector3();
            var step = this is LineSegments ? 2 : 1;
            var index = geometry.index;
            var attributes = geometry.attributes;
            var positionAttribute = attributes.position;
            if (index != null)
            {
                int start = Math.Max(0, drawRange.start);
                int end = Math.Min(index.count, (drawRange.start + drawRange.count));
                for (int i = start, l = end - 1; i < l; i += step)
                {
                    var a = index.getIntX(i);
                    var b = index.getIntX(i + 1);
                    vStart.FromBufferAttribute(positionAttribute, a);
                    vEnd.FromBufferAttribute(positionAttribute, b);
                    var distSq = _ray.DistanceSqToSegment(vStart, vEnd, interRay, interSegment);
                    if (distSq > localThresholdSq) continue;
                    interRay.ApplyMatrix4(this.matrixWorld); //Move back to world space for distance calculation
                    var distance = raycaster.ray.Origin.DistanceTo(interRay);
                    if (distance < raycaster.near || distance > raycaster.far) continue;
                    intersects.Push(new Raycaster.Intersection
                    {
                        distance = distance,
                        // What do we want? intersection point on the ray or on the segment??
                        // point: raycaster.ray.at( distance ),
                        point = interSegment.Clone().ApplyMatrix4(this.matrixWorld),
                        index = i,
                        face = null,
                        faceIndex = -1,
                        target = this
                    });
                }
            }
            else
            {
                var start = Math.Max(0, drawRange.start);
                var end = Math.Min(positionAttribute.count, (drawRange.start + drawRange.count));
                for (int i = start, l = end - 1; i < l; i += step)
                {
                    vStart.FromBufferAttribute(positionAttribute, i);
                    vEnd.FromBufferAttribute(positionAttribute, i + 1);
                    var distSq = _ray.DistanceSqToSegment(vStart, vEnd, interRay, interSegment);
                    if (distSq > localThresholdSq) continue;
                    interRay.ApplyMatrix4(this.matrixWorld); //Move back to world space for distance calculation
                    var distance = raycaster.ray.Origin.DistanceTo(interRay);
                    if (distance < raycaster.near || distance > raycaster.far) continue;
                    intersects.Push(new Raycaster.Intersection
                    {
                        distance = distance,
                        // What do we want? intersection point on the ray or on the segment??
                        // point: raycaster.ray.at( distance ),
                        point = interSegment.Clone().ApplyMatrix4(this.matrixWorld),
                        index = i,
                        face = null,
                        faceIndex = -1,
                        target = this

                    });
                }
            }
        }
        public void updateMorphTargets()
        {
            var geometry = this.geometry;
            var morphAttributes = geometry.morphAttributes;
            var keys = morphAttributes.Keys.ToListEx(); //Object.keys(morphAttributes);
            if (keys.Count > 0)
            {
                var morphAttribute = morphAttributes[keys[0]];
                if (morphAttribute != null)
                {
                    this.morphTargetInfluences = new ListEx<double>();
                    this.morphTargetDictionary = new JsObj<int>();
                    for (int m = 0, ml = morphAttribute.Length; m < ml; m++)
                    {
                        var name = morphAttribute[m]?.name ?? (m).ToString();
                        this.morphTargetInfluences.Push(0);
                        this.morphTargetDictionary[name] = m;
                    }
                }
            }
        }

        public ListEx<Material> getMaterials()
        {
            return materials;
        }

        public void setMaterials(ListEx<Material> materials)
        {
            this.materials = materials;
        }
        public bool isMultiMaterial()
        {
            return this.materials.Count > 1;
        }
        public Material getMaterial()
        {
            return this.material;
        }

        public void setMaterial(Material material)
        {
            this.material = material;
        }

        public BufferGeometry getGeometry()
        {
            return geometry;
        }

        public void setGeometry(BufferGeometry geo)
        {
            this.geometry = geo;
        }
        #endregion

    }
}
